package BitCreeekClient;

/**
 * @author Ivan Litsvinenka & Emanuele Ricci
 * Classe che rappresenta il comportamento e le informazioni utilizzate dai Peer per il download di un Torrent
 *
 */

import BitCreekLib.Host;
import BitCreekLib.MetaInfo;

import java.io.*;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.*;

public class Download implements IDownload {

    boolean firstPartsUpdate;

    /**
     * Le informazioni relative al torrent
     */
    MetaInfo metaInfo;

    /**
     * Il path su cui verra' salvato il file
     */
    String localPath;

    /**
     * Quanti parti sono possedute
     */
    int possesedParts;

    /**
     * IP del tracker
     */
    String trackerIP;

    /**
     * Array di Parts del torrent
     */
    Part parts[];

    /**
     * Lista dei Peer a cui siamo connessi per scaricare e uploadare le parti del torrent
     */
    protected ArrayList<Host> connectedPeers = new ArrayList<Host> ();

    /**
     * Lista dei peers appartentendi allo Swarm di un determinato torrent
     */
    protected Hashtable<Host, PeerInfo> swarmInfo = new Hashtable<Host, PeerInfo> ();

    private Random randomGenerator;
    
    protected static int MAX_CONNECTIONS; // N
    protected static int INIT_CONNECTIONS; // K
    protected static int currentConnectionsCount;

    public Download () {
        MAX_CONNECTIONS = 20;
        INIT_CONNECTIONS = 5;
        currentConnectionsCount = 0;
        this.randomGenerator = new Random ();
    }

    /*
       Stores bytes in local file
   * */
    public void storePart ( int index, byte[] bytes ) {

        if ( Utils.getHash ( bytes ).equals ( this.metaInfo.getHash ( index ) ) ) {
            RandomAccessFile f;
            try {
                f = new RandomAccessFile ( this.localPath, "rw" );
                f.seek ( PARTSIZE * index );
                f.write ( bytes );
                f.close ();
            } catch ( IOException ex ) {
                ex.printStackTrace ();
                applicationWindow.c.fireGUIError ( new GUIError ( ex ) );
            }
        }
    }

    public byte[] recoverPart ( int index ) {
        RandomAccessFile f;
        byte[] buffer = new byte[this.parts[ index ].getSize ()];
        try {
            f = new RandomAccessFile ( this.localPath, "r" );
            f.seek ( PARTSIZE * index );
            f.read ( buffer );
            f.close ();
        } catch ( IOException ex ) {
            ex.printStackTrace ();
        }
        return buffer;
    }

    /*
      * Creates new Download from local file
      */
    public void torrentFormFile ( String localPath ) throws Exception {

        RandomAccessFile f;
        byte[] buffer;
        int index, bytesRead, bufferSize;

        this.localPath = localPath;
        File file = new File ( localPath );

        int numparts = ( file.length () % PARTSIZE == 0 ) ? ( ( int ) file.length () / PARTSIZE ) : ( int ) Math.floor ( file.length () / PARTSIZE ) + 1;
        metaInfo = new MetaInfo ( file.getName (), file.length (), numparts, this.trackerIP );
        parts = new Part[this.metaInfo.getNumParts ()];
        metaInfo.hashes = new String[this.metaInfo.getNumParts ()];
        possesedParts = this.metaInfo.getNumParts ();

        f = new RandomAccessFile ( localPath, "r" );
        for ( index = 0; index < numparts; index++ ) {
            bufferSize = ( index == numparts - 1 ) ? ( ( int ) ( metaInfo.getSize () % PARTSIZE ) ) : PARTSIZE;
            buffer = new byte[bufferSize];
            try {
                f.seek ( PARTSIZE * index );
                bytesRead = f.read ( buffer );
                parts[ index ] = new Part ( bytesRead, PartStatusType.COMPLETE );
                metaInfo.hashes[ index ] = Utils.getHash ( buffer );
            } catch ( IOException ex ) {
                f.close ();
                applicationWindow.c.fireGUIError ( new GUIError ( ex ) );
            }
        }
        f.close ();
    }

    public void torrentFromMetaInfo ( MetaInfo metaInfo, String destinationPath ) {

        this.metaInfo = metaInfo;
        localPath = destinationPath;
        parts = new Part[this.metaInfo.getNumParts ()];
        for ( int i = 0; i < parts.length; i++ ) {
            if ( i == metaInfo.getNumParts () - 1 )
                parts[ i ] = new Part ( ( int ) ( metaInfo.getSize () % PARTSIZE ), PartStatusType.INCOMPLETE );
            else parts[ i ] = new Part ( PARTSIZE, PartStatusType.INCOMPLETE );
        }
        possesedParts = 0;
    }

    /*
      * Saves Metainfo to local file descriptor
      */
    public void saveMetaInfo ( String describtorPath ) {
        FileOutputStream fos;
        ObjectOutputStream out;
        try {
            fos = new FileOutputStream ( describtorPath );
            out = new ObjectOutputStream ( fos );
            out.writeObject ( this.metaInfo );
            out.writeObject ( this.localPath );
            out.close ();
        } catch ( IOException ex ) {
        	applicationWindow.c.fireGUIError ( new GUIError ( ex ) );
        }
    }

    public void loadMetaInfo ( String localPath ) throws IOException, ClassNotFoundException {
        FileInputStream fis;
        ObjectInputStream in;
        fis = new FileInputStream ( localPath );
        in = new ObjectInputStream ( fis );
        this.metaInfo = ( MetaInfo ) in.readObject ();
        this.localPath = ( String ) in.readObject ();
        this.parts = new Part[this.metaInfo.getNumParts ()];
        this.possesedParts = 0;
        this.updateLocalFileStatus ();
        this.trackerIP = metaInfo.getTrackerAddress ().getHostAddress ();

    }

    /*
      * Begins download of file, updates status of possesed parts
      */


    public void initDownload () {

        try {
            RandomAccessFile f = new RandomAccessFile ( this.localPath, "rw" );
            /* File is already created? */
            if ( f.length () != this.metaInfo.getSize () ) {
                f.setLength ( this.metaInfo.getSize () );
            }
            /* Getting progression */
            updateLocalFileStatus ();
            f.close ();
        } catch ( IOException ex ) {
        	applicationWindow.c.fireGUIError ( new GUIError ( ex ) );
        }
        this.firstPartsUpdate = true;
    }

    public void analizeAvaibility () {
        resetAvailability ();
        for ( Host connectedPeer : connectedPeers ) {
            Part[] peerParts = swarmInfo.get ( connectedPeer ).getPeerParts ();
            for ( int j = 0; j < peerParts.length; j++ ) {
                if ( peerParts[ j ].getStatus ().equals ( PartStatusType.COMPLETE ) ) this.parts[ j ].availability++;
            }
        }
    }

    private void resetAvailability () {
        for ( Part part : parts ) {
            part.availability = 0;
        }

    }
    
    private int randomBestPart( int max ) {
        int r;
        int range = max - 0 + 1;
        r = randomGenerator.nextInt ( range );
        return r;
    }

    public int getRarestPart () {
        int min = -1;
        for ( int i = 0; i < parts.length; i++ ) {
        	if ( parts[ i ].availability == 0 )
        		continue; //nessuno ha questa parte.
            if ( min == -1 && parts[ i ].getStatus ().equals(PartStatusType.INCOMPLETE)  )
                min = i;
            
            if ( parts[ i ].getStatus ().equals(PartStatusType.INCOMPLETE)  && parts[ i ].availability < parts[ min ].availability )
                min = i;
        }
        
        if ( min == -1 ) // non ha trovato nessuna parte disponibile
        	return min;
        
        // A questo punto so' quale e' l'avaibility minima
        ArrayList<Integer> ap = new ArrayList<Integer>();
        int minAV = parts[ min ].availability;
        
        for ( int i = 0; i < parts.length; i++ ) {
        	if ( parts[ i ].getStatus ().equals(PartStatusType.INCOMPLETE) && parts[ i ].availability == minAV  )
        		ap.add(i);
        }
        
        return ap.get( randomBestPart( ap.size() -1 ) );
    }

    /*
      * Populates Parts of Download from local file
      */
    public void updateLocalFileStatus () throws IOException {
        FileInputStream fis;
        BufferedInputStream bis;
        DataInputStream dis;
        int bytesRead, bufferSize;
        int i = 0;
        byte[] bytes = new byte[PARTSIZE];


        fis = new FileInputStream ( this.localPath );
        bis = new BufferedInputStream ( fis );
        dis = new DataInputStream ( bis );

        while ( ( bytesRead = dis.read ( bytes ) ) != -1 ) {
            if ( this.metaInfo.hashes[ i ].equals ( Utils.getHash ( bytes ) ) ) {
                parts[ i ] = new Part ( bytesRead, PartStatusType.COMPLETE );
                this.possesedParts++;
            } else {
                parts[ i ] = new Part ( bytesRead, PartStatusType.INCOMPLETE );
            }
            i++;
            bufferSize = ( i == this.metaInfo.getNumParts () - 1 ) ? ( ( int ) ( metaInfo.getSize () % PARTSIZE ) ) : PARTSIZE;
            bytes = new byte[bufferSize];
        }
        fis.close ();
        bis.close ();
        dis.close ();
        applicationWindow.c.fireGUIEvent ( new GUIEvent ( this ) );
    }

    public int getProgress () {
        if ( metaInfo.getNumParts () == 0 ) return 0;
        return ( 100 * this.possesedParts ) / this.metaInfo.getNumParts ();
    }

    public String getName () {
        return this.metaInfo.getName ();
    }

    public Hashtable<Host, PeerInfo> getSwarm () {
        return swarmInfo;
    }

    public ArrayList<Host> getSwarmHostList () {
        return ( ArrayList<Host> ) swarmInfo.keys ();
    }

    public void setSwarm ( ArrayList<Host> newSwarm ) {

        boolean find = false;

        for ( Host h : newSwarm ) {
        	
        	find = false;
        	
            for ( Host h2 : swarmInfo.keySet () ) {
                if ( h.equals ( h2 ) ) {
                    find = true;
                    break;
                }

            }
            if ( !find ) {
                try {
                    if ( h.getAddress ().equals ( InetAddress.getByName ( InetAddress.getLocalHost ().getHostAddress () ) ) )
                        continue;
                    Socket peerSocket = new Socket ( h.getAddress (), h.getPort () );
                    peerSocket.setKeepAlive ( true );
                    PeerInfo newPeer = new PeerInfo ( peerSocket, null );
                    swarmInfo.put ( h, newPeer );
                } catch ( SocketException e ) {
                	applicationWindow.c.fireGUIError ( new GUIError ( e ) );
                } catch ( IOException e ) {
                	applicationWindow.c.fireGUIError ( new GUIError ( e ) );
                }
                break;
            }
        }

        Enumeration htk = swarmInfo.keys ();
        while ( htk.hasMoreElements () ) {
            Host h = ( Host ) htk.nextElement ();
            if ( !newSwarm.contains ( h ) ) {

                connectedPeers.remove ( h );

                //chiudo socket
                try {
                    swarmInfo.get(h).getSocket().close();
                } catch (NullPointerException e) {
                	e.printStackTrace();
				} 
                catch (IOException e) {
                	applicationWindow.c.fireGUIError ( new GUIError ( e ) ); 
                }
                currentConnectionsCount--;
                swarmInfo.remove ( h );
            }
        }


    }


    public String toString () {
        return "name:\t" + this.metaInfo.getName () + "\n size:\t" + Utils.humanSize ( this.metaInfo.getSize () ) + "\n local:\t" + localPath + "\n parts:\t" + this.metaInfo.getNumParts () + "\n Progress" + this.getProgress ();
    }

    public Vector<String> vectorize () {
        Vector<String> result = new Vector<String> ();
        result.add ( getName () );
        result.add ( Utils.humanSize ( metaInfo.getSize () ) );
        result.add ( String.valueOf ( getPossesedParts () ) + " / " + String.valueOf ( metaInfo.getNumParts () ) );
        result.add ( trackerIP );
        result.add ( String.valueOf ( this.getProgress () ) );
        result.add ( String.valueOf ( connectedPeers.size () ) + " / " + String.valueOf ( swarmInfo.size () ) );
        result.add ( localPath );
        result.add ( String.valueOf ( getUploadBand () ) );
        result.add ( String.valueOf ( getDownloadBand () ) );
        return result;
    }

    public Host bestNextPeer () {
        Host bestPeer = null;
        for ( Map.Entry<Host, PeerInfo> hp : getSwarm ().entrySet () ) {
            
        	
        	
        	if ( !connectedPeers.contains ( hp.getKey () ) && bestPeer == null ) {
                bestPeer = hp.getKey ();
            }
            if ( !connectedPeers.contains ( hp.getKey () ) && ( hp.getValue ().getUploaded () > getSwarm ().get ( bestPeer ).getUploaded () ) ) {
                bestPeer = hp.getKey ();
            }
        }
        return bestPeer;
    }

    public void updateSwarmAvaibility () {
        while ( currentConnectionsCount < INIT_CONNECTIONS ) {
            Host currentHost = bestNextPeer();
            applicationWindow.c.fireGUIEvent ( new GUIEvent ( this ) );
            if ( currentHost == null ) break;
            try {
                PeerInfo currentPeerInfo = swarmInfo.get ( currentHost );


                if ( currentPeerInfo.getSocket () == null ) {
                    Socket newSocket = new Socket ( currentHost.getAddress (), currentHost.getPort () );
                    newSocket.setKeepAlive ( true );
                    swarmInfo.get ( currentHost ).setSocket ( newSocket );
                    currentPeerInfo.setSocket ( newSocket );
                }


                

                currentPeerInfo.setConnected ( true );
                currentConnectionsCount++;


                Socket socket = currentPeerInfo.getSocket ();

                ObjectOutputStream output = new ObjectOutputStream ( socket.getOutputStream () );
                ObjectInputStream input = new ObjectInputStream ( socket.getInputStream () );
                output.writeObject ( new P2P_Packet ( P2P_MessageType.GET_PARTS, null ) );
                P2P_Packet reply = ( P2P_Packet ) input.readObject ();

                if ( !reply.getType ().equals ( P2P_MessageType.ERROR ) ) {
                    connectedPeers.add ( currentHost );
                    swarmInfo.put ( currentHost, new PeerInfo ( socket, ( Part[] ) reply.getValue () ) );

                } else {
                    currentPeerInfo.setConnected ( false );
                    currentConnectionsCount--;
                }
                applicationWindow.c.fireGUIEvent ( new GUIEvent ( this ) );
            } catch ( ConnectException e ) {
                Log.add ( "Host: " + currentHost.toString() + " is unavaible removing from Swarm");
                swarmInfo.remove ( currentHost );
            } catch ( Exception e ) {
            	e.printStackTrace();
            	//applicationWindow.c.fireGUIError ( new GUIError ( e ) );
            }
            
        }
    }

    public byte[] getData ( int i ) {
        return recoverPart ( i );
    }

    public void setData ( int i, byte[] tmp ) {
        storePart ( i, tmp );
    }

    public boolean canAcceptMoreConnections ( InetAddress inetAddress ) {
        // Is host from my connectedPeers?
        if ( connectedPeers.contains ( new Host ( inetAddress, 0 ) ) ) return true;
        // Got free slots?
        return MAX_CONNECTIONS > currentConnectionsCount;
    }

    public String getUploadBand () {
        float result = 0;

        for ( Host connectedPeer : connectedPeers ) {
            if ( ( System.currentTimeMillis () - getSwarm ().get ( connectedPeer ).getFirstTimeDownloaded () ) != 0 )
                result += getSwarm ().get ( connectedPeer ).getDownloaded () / ( ( System.currentTimeMillis () - getSwarm ().get ( connectedPeer ).getFirstTimeDownloaded () ) );
        }
        return Utils.humanSize ( ( int ) result * 1000 );
    }

    public String getDownloadBand () {
        if ( getProgress () == 100 || getProgress () == 0 ) return String.valueOf ( 0 );
        float result = 0;
        for ( Host connectedPeer : connectedPeers ) {
            if ( ( System.currentTimeMillis () - getSwarm ().get ( connectedPeer ).getFirstTimeUploaded () ) != 0 )
                result += getSwarm ().get ( connectedPeer ).getUploaded () / ( ( System.currentTimeMillis () - getSwarm ().get ( connectedPeer ).getFirstTimeUploaded () ) );
        }
        return Utils.humanSize ( ( int ) result * 1000 );
    }

    public ArrayList<Host> getConnectedPeers () {
        return connectedPeers;
    }

    public void setConnectedPeers ( ArrayList<Host> connectedPeers ) {
        this.connectedPeers = connectedPeers;
    }

    public int getPossesedParts () {
        return possesedParts;
    }

}
